/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity;
using Leap.Unity.Query;
using UnityEngine;
namespace Leap.Examples
{
public class InertiaPostProcessProvider : PostProcessProvider
{
[Header("Inertia")]
[Tooltip("Higher stiffness will keep the bouncy hand closer to the tracked hand data.")]
[Range(0f, 10f)]
public float stiffness = 2f;
[Tooltip("Higher damping will suppress more motion and reduce oscillation.")]
[Range(0f, 10f)]
public float damping = 2f;
// Update-time Hand Data
private Pose? _leftPose = null;
private Pose? _previousLeftPose = null;
private float _leftAge = 0f;
private Pose? _rightPose = null;
private Pose? _previousRightPose = null;
private float _rightAge = 0f;
// FixedUpdate-time Hand Data
private Pose? _fixedLeftPose = null;
private Pose? _fixedPreviousLeftPose = null;
private float _fixedLeftAge = 0f;
private Pose? _fixedRightPose = null;
private Pose? _fixedPreviousRightPose = null;
private float _fixedRightAge = 0f;
///
/// Post-processes the input frame in place to give hands bouncy-feeling physics.
///
public override void ProcessFrame(ref Frame inputFrame)
{
var leftHand = inputFrame.Hands.Query().FirstOrDefault(h => h.IsLeft);
var rightHand = inputFrame.Hands.Query().FirstOrDefault(h => !h.IsLeft);
// Frames can potentially come from two time-interwoven sources: Update frames
// and FixedUpdate frames. Time is not monotonically increasing frame-to-frame
// because FixedUpdates and Updates interweave and occasionally FixedUpdate plays
// catch-up, and we interpolate hand data accordingly further up the hand pipeline,
// which affects the hand.TimeVisible property we use to simulate our effect
// statefully over time.
//
// To support both Update-time hand data and FixedUpdate-time hand data with a
// single stateful post-process, we maintain two independent states for each stream,
// which, independently, _are_ going to be monotonically forward-moving in time.
if (Time.inFixedTimeStep)
{
// FixedUpdate hand data.
processHand(leftHand,
ref _fixedLeftPose, ref _fixedPreviousLeftPose, ref _fixedLeftAge);
processHand(rightHand,
ref _fixedRightPose, ref _fixedPreviousRightPose, ref _fixedRightAge);
}
else
{
// Update hand data.
processHand(leftHand, ref _leftPose, ref _previousLeftPose, ref _leftAge);
processHand(rightHand, ref _rightPose, ref _previousRightPose, ref _rightAge);
}
}
private void processHand(Hand hand,
ref Pose? maybeCurPose,
ref Pose? maybePrevPose,
ref float handAge)
{
if (hand == null)
{
// Clear state.
maybeCurPose = null;
maybePrevPose = null;
handAge = 0f;
}
else
{
var framePose = hand.GetPalmPose();
if (!maybeCurPose.HasValue)
{
// The hand just started being tracked.
maybePrevPose = null;
maybeCurPose = framePose;
}
else if (!maybePrevPose.HasValue)
{
// Have current pose, lack previous pose, just get initial momentum.
maybePrevPose = maybeCurPose;
maybeCurPose = framePose;
}
else
{
// There's enough data to verlet-integrate.
// Calculate how much time has passed since we last received hand data.
//
// As a safety measure, we ensure deltaTime is positive before running our
// stateful filter to give the hand momentum. Any post-process could mess with
// the TimeVisible property, so we do this to minimize the chance of total
// havok.
var deltaTime = hand.TimeVisible - handAge;
if (deltaTime > 0)
{
handAge = hand.TimeVisible;
var curPose = maybeCurPose.Value;
var prevPose = maybePrevPose.Value;
integratePose(ref curPose, ref prevPose,
targetPose: framePose, deltaTime: deltaTime);
hand.SetPalmPose(curPose);
maybeCurPose = curPose;
maybePrevPose = prevPose;
}
}
}
}
///
/// Integrates curPose's inertia from prevPose to give it bouncy-feeling physics
/// while gradually shifting it towards the target pose.
///
private void integratePose(ref Pose curPose, ref Pose prevPose,
Pose targetPose, float deltaTime)
{
// Calculate motion from prevPose to curPose.
var deltaPose = curPose.inverse().mul(prevPose); // prevPose in curPose's local space.
deltaPose = new Pose(-deltaPose.position, Quaternion.Inverse(deltaPose.rotation));
deltaPose = deltaPose.Lerp(Pose.identity, damping * deltaTime); // Dampen.
// Verlet-integrate curPose based on the delta from prevPose.
Pose tempPose = curPose;
curPose = curPose.mul(deltaPose);
prevPose = tempPose;
// Pull the integrated hand toward the target a little bit based on stiffness.
curPose = curPose.Lerp(targetPose, stiffness * deltaTime);
}
}
}